<?php

declare( strict_types=1 );

namespace simpleHandle\Component\UtilRedis;

use simpleHandle\Component\UtilTool\Tool;
use simpleHandle\Exception\UtilException;
use Throwable;
use Redis;

class BaseRedis
{
	protected       $pool;
	protected Redis $connection;
	protected bool  $multiOnGoing = false;

	/**
	 * @throws UtilException
	 */
	public function __construct( $config = null, $poolName = 'default' ) {
		try {
			$this->pool = Connection::getInstance( $config, $poolName );
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function __call( $name, $arguments ) {
		try {
			if ( ! $this->multiOnGoing ) {
				$this->connection = $this->pool->getConnection();
			}
			$data = $this->connection->{$name}( ...$arguments );
			if ( $this->multiOnGoing ) {
				return $this;
			}

			return $data;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		} finally {
			$this->pool->close( $this->connection ?? null );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function brPop( $keys, $timeout ): array {
		try {
			$this->connection = $this->pool->getConnection();
			$data             = $this->connection->brPop( $keys, $timeout );
			$this->pool->close( $this->connection );

			return $data;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function blPop( $keys, $timeout ): array {
		try {
			$this->connection = $this->pool->getConnection();

			return $this->connection->blPop( $keys, $timeout );
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		} finally {
			$this->pool->close( $this->connection );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function subscribe( $channels, $callback ) {
		try {
			$this->connection = $this->pool->getConnection();
			$this->connection->setOption( Redis::OPT_READ_TIMEOUT, '-1' );
			$data = $this->connection->subscribe( $channels, $callback );
			$this->connection->setOption( Redis::OPT_READ_TIMEOUT, (string) $this->pool->getConfig()['time_out'] );
			$this->pool->close( $this->connection );

			return $data;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function fill() {
		try {
			$this->pool->fill();
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function multi( $mode = Redis::MULTI ): BaseRedis {
		try {
			if ( ! $this->multiOnGoing ) {
				$this->connection = $this->pool->getConnection();
				$this->connection->multi( $mode );
				$this->multiOnGoing = true;
			}

			return $this;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function exec() {
		try {
			if ( ! $this->multiOnGoing ) {
				return;
			}
			$result             = $this->connection->exec();
			$this->multiOnGoing = false;
			$this->pool->close( $this->connection );

			return $result;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function discard() {
		try {
			if ( ! $this->multiOnGoing ) {
				return;
			}
			$this->pool->close( $this->connection );
			$this->multiOnGoing = false;
			$this->connection->discard();
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function lock( $key, $ttl = 60 ) {
		try {
			$this->connection = $this->pool->getConnection();
			$randNum          = Tool::character( 32 );
			$result           = $this->connection->set( $key, $randNum, [ 'NX', 'PX' => $ttl * 1000 ] );
			if ( $result ) {
				return $randNum;
			}

			return null;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		} finally {
			$this->pool->close( $this->connection );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function unlock( $key, $randNum ) {
		try {
			$lua              = <<<LUA
								if redis.call('get',KEYS[1]) == ARGV[1] then 
								   return redis.call('del',KEYS[1]) 
								else
								   return 0 
								end
								LUA;
			$this->connection = $this->pool->getConnection();

			return $this->connection->eval( $lua, [ $key, $randNum ], 1 );
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		} finally {
			$this->pool->close( $this->connection );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function lockFunction( $key, callable $callback, $ttl = 60 ) {
		try {
			$random = $this->lock( $key, $ttl );
			if ( $random === null ) {
				return false;
			}
			$result = call_user_func( $callback );
			$this->unlock( $key, $random );

			return $result;
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		}
	}

	/**
	 * @throws UtilException
	 */
	public function rateLimit( $key, $limitNum = 200, $ttl = 5 ) {
		try {
			$lua              = <<<SCRIPT
									            redis.call('zAdd',KEYS[1],tonumber(ARGV[2]),ARGV[3])
									            redis.call('zRemRangeByScore',KEYS[1],0,tonumber(ARGV[2])-tonumber(ARGV[1]))
									            redis.call('expire',KEYS[1],tonumber(ARGV[1]))
									            local num = redis.call('zCount',KEYS[1],0,tonumber(ARGV[2]))
									            if num > tonumber(ARGV[4]) then
									                return 1
									            else
									                return 0
									            end
									SCRIPT;
			$this->connection = $this->pool->getConnection();
			$score            = time();
			$nonce            = Tool::character( 32 );

			return $this->connection->eval( $lua, [ $key, $ttl, $score, $nonce, $limitNum ], 1 );
		} catch ( Throwable $th ) {
			throw new UtilException( $th->getMessage(), UtilException::EasyRedis_ERROR_CODE );
		} finally {
			$this->pool->close( $this->connection );
		}
	}
}
